﻿using System;
using System.Net;
using System.Net.Sockets;
using System.Diagnostics;

namespace AsyncEvtEcho
{
    sealed class EchoServer : IDisposable
    {
        // *************************************************************** //
        #region [ member fields ]

        private readonly Socket m_lsnSocket;
        private readonly SocketAsyncEventArgs m_acceptEvtArgs;
        private IEvtArgsPool m_evtArgsPool;

        #endregion

        // *************************************************************** //
        #region [ constructor ]

        /// <summary>
        /// because the SocketAsyncEventArgs need to be re-used, so here
        /// we pass in a factory, which will attach the event handler when generate those SocketAsyncEventArgs
        /// and will never change them afterwards
        /// </summary>
        /// <param name="poolFactory"></param>
        public EchoServer(Func<EventHandler<SocketAsyncEventArgs>, IEvtArgsPool> poolFactory)
        {
            m_evtArgsPool = poolFactory(OnIOCompleted);

            m_lsnSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            m_acceptEvtArgs = new SocketAsyncEventArgs();
            m_acceptEvtArgs.Completed += this.OnAccepted;
        }

        #endregion

        // *************************************************************** //
        #region [ destructor ]

        private bool m_disposed;

        ~EchoServer()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (m_disposed)
                return;

            try
            {
                if (disposing)
                {
                    m_acceptEvtArgs.Completed -= this.OnAccepted;
                    m_acceptEvtArgs.Dispose();
                    m_lsnSocket.Close();
                }
            }
            finally
            {
                m_disposed = true;
            }
        }

        #endregion

        // *************************************************************** //
        #region [ public APIs ]

        public void Run(int lsnPort)
        {
            m_lsnSocket.Bind(new IPEndPoint(IPAddress.Any, lsnPort));
            m_lsnSocket.Listen(10);
            StartAccept();
        }

        #endregion

        // *************************************************************** //
        #region [ private helpers ]

        private void StartAccept()
        {
            m_acceptEvtArgs.AcceptSocket = null;// clean for re-use

            if (!m_lsnSocket.AcceptAsync(m_acceptEvtArgs))
            {
                // accepted synchronously
                OnAccepted(null, m_acceptEvtArgs);
            }
        }

        private void OnAccepted(object sender, SocketAsyncEventArgs acceptEvtArgs)
        {
            // ----------- launch async-receive on the newly-accepted socket
            if (acceptEvtArgs.SocketError == SocketError.Success)
            {
                SvrResponse response = new SvrResponse(acceptEvtArgs.AcceptSocket);

                SocketAsyncEventArgs ioEvtArgs = m_evtArgsPool.Pop();
                ioEvtArgs.UserToken = response;

                response.StartReceive(ioEvtArgs, ProcessReceive);

                // ----------- launch async-accept once again
                StartAccept();
            }
            else
                throw new SocketException((int)acceptEvtArgs.SocketError);
        }

        private void OnIOCompleted(object sender, SocketAsyncEventArgs evtargs)
        {
            switch (evtargs.LastOperation)
            {
                case SocketAsyncOperation.Receive:
                    ProcessReceive(evtargs);
                    break;

                case SocketAsyncOperation.Send:
                    ProcessSend(evtargs);
                    break;

                default:
                    throw new ArgumentOutOfRangeException("un-recognized last socket operation");
            }
        }

        private void ProcessReceive(SocketAsyncEventArgs evtArgs)
        {
            SvrResponse response = (SvrResponse)evtArgs.UserToken;

            if (evtArgs.BytesTransferred > 0 && evtArgs.SocketError == SocketError.Success)
            {
                if (response.OnReceived(evtArgs.Buffer, evtArgs.Offset, evtArgs.BytesTransferred))
                {
                    response.StartReply(evtArgs, ProcessSend);
                }
                else
                {
                    response.StartReceive(evtArgs, ProcessReceive);// received not sufficient, receive more
                }//else
            }
            else if (evtArgs.BytesTransferred == 0)
            {
                Console.WriteLine("client[{0}] active close", response.ClientEndPoint);
                CloseClient(evtArgs);
            }
            else if (evtArgs.SocketError != SocketError.Success)
            {
                Console.WriteLine("client[{0}] receive error: {1}",
                    response.ClientEndPoint, (new SocketException((int)evtArgs.SocketError)).Message);
                CloseClient(evtArgs);
            }
            else
                throw new NotImplementedException("unhandled receive case");
        }

        private void ProcessSend(SocketAsyncEventArgs evtArgs)
        {
            SvrResponse response = (SvrResponse)evtArgs.UserToken;

            if (evtArgs.SocketError == SocketError.Success)
            {
                if (evtArgs.BytesTransferred != evtArgs.Buffer.Length)
                    throw new NotImplementedException("cannot send all in a single shot");
                response.StartReceive(evtArgs, ProcessReceive);// receive next
            }
            else
            {
                Console.WriteLine("client[{0}] send error: {1}",
                    response.ClientEndPoint, (new SocketException((int)evtArgs.SocketError)).Message);
                CloseClient(evtArgs);
            }
        }

        private void CloseClient(SocketAsyncEventArgs evtargs)
        {
            SvrResponse response = (SvrResponse)evtargs.UserToken;
            response.Dispose();

            evtargs.UserToken = null;// clean and accelerate GC
            m_evtArgsPool.Push(evtargs);// for re-use
        }

        #endregion
    }
}
